YAML:可能並不是那麼完美
我之前寫過為什麼將 JSON 用於人類可編輯的配置文件是一個壞主意,今天我們將討論 YAML 格式的一些常見問題。
默認情況下不安全
YAML 默認是不安全的。載入用戶提供的(不可信的)YAML 字元串需要仔細考慮。
!!python/object/apply:os.system
args: ['ls /']
用 print(yaml.load(open('a.yaml')))
運行它,應該給你這樣的東西:
bin etc lib lost+found opt root sbin tmp var sys
boot dev efi home lib64 mnt proc run srv usr
0
許多其他語言(包括 Ruby 和 PHP 1 )默認情況下也不安全(LCTT 譯註:這裡應該說的是解析 yaml)。在 GitHub 上搜索 yaml.load 會得到驚人的 280 萬個結果,而 yaml.safe_load 只能得到 26000 個結果。
提個醒,很多這樣的 yaml.load()
都工作的很好,在配置文件中載入 yaml.load()
通常沒問題,因為它通常(雖然並不總是!)來自「可靠源」,而且很多都來自靜態的 YAML 測試文件。但是,人們還是不禁懷疑在這 280 萬個結果中隱藏了多少漏洞。
這不是一個理論問題。在 2013 年,正是由於這個問題,所有的 Ruby on Rails 應用程序都被發現易受遠程代碼執行攻擊。
有人可能會反駁說這不是 YAML 格式的錯誤,而是那些庫實現錯誤的的問題,但似乎大多數庫默認不是安全的(特別是動態語言),所以事實上這是 YAML 的一個問題。
有些人可能會反駁認為修復它就像用 safe_load()
替換 load()
一樣容易,但是很多人都沒有意識到這個問題,即使你知道它,它也是很容易忘記的事情之一。這是非常糟糕的 API 設計。
可能很難編輯,特別是對於大文件
YAML 文件可能很難編輯,隨著文件變大,這個難度會快速增大。
一個很好的例子是 Ruby on Rails 的本地化翻譯文件。例如:
en:
formtastic:
labels:
title: "Title" # Default global value
article:
body: "Article content"
post:
new:
title: "Choose a title..."
body: "Write something..."
edit:
title: "Edit title"
body: "Edit body"
看起來不錯,對吧?但是如果這個文件有 100 行怎麼辦?或者 1,000 行?在文件中很難看到 「where」,因為它可能在屏幕外。你需要向上滾動,但是你需要跟蹤縮進,即使遵循縮進指南也很難,特別是因為 2 個空格縮進是常態而且 製表符縮進被禁止 2 。
不小心縮進出錯通常不算錯誤,它通常只是反序列化為你不想要的東西。這樣只能祝你調試快樂!
我已經愉快地編寫 Python 長達十多年,所以我已經習慣了顯眼的空白,但有時候我仍在和 YAML 抗爭。在 Python 中,雖然沒有那種長達幾頁的函數,但數據或配置文件的長度沒有這種自然限制,這就帶來了缺點和損失了清晰度。
對於小文件,這不是問題,但它確實無法很好地擴展到較大的文件,特別是如果你以後想編輯它們的話。
這非常複雜
在瀏覽一個基本的例子時,YAML 看似「簡單」和「顯而易見」,但事實證明並非如此。YAML 規範有 23449 個單詞,為了比較,TOML 有 3339 個單詞,Json 有 1969 個單詞,XML 有 20603 個單詞。
我們中有誰讀過全部規範嗎?有誰讀過並理解了全部?誰閱讀過,理解進而記住所有這些?
例如,你知道在 YAML 中編寫多行字元串有 9 種方法嗎?並且它們具有細微的不同行為。
是的 :-/
如果你看一下那篇文章的修訂歷史,它就會變得更加有趣,因為文章的作者發現了越來越多的方法可以實現這一點,以及更多的細微之處。
它從預覽開始告訴我們 YAML 規範,它表明(強調我的):
本節簡要介紹了 YAML 的表達能力。預計初次閱讀的人不可能理解所有的例子。相反,這些選擇用作該規範其餘部分的動機。
令人驚訝的行為
以下會解析成什麼(Colm O』Connor 提供的例子):
- Don Corleone: Do you have faith in my judgment?
- Clemenza: Yes
- Don Corleone: Do I have your loyalty?
結果為:
[
{'Don Corleone': 'Do you have faith in my judgment?'},
{'Clemenza': True},
{'Don Corleone': 'Do I have your loyalty?'}
]
那麼這個呢:
python: 3.5.3
postgres: 9.3
3.5.3
被識別為字元串,但 9.3
被識別為數字而不是字元串:
{'python': '3.5.3', 'postgres': 9.3}
這個呢:
Effenaar: Eindhoven
013: Tilburg
013
是 蒂爾堡 的一個流行音樂場地,但 YAML 會告訴你錯誤答案,因為它被解析為八進位數字:
{11: 'Tilburg', 'Effenaar': 'Eindhoven'}
所有這一切,以及更多,就是為什麼許多經驗豐富的 YAMLer 經常會將所有字元串用引號引起來的原因,即使它不是嚴格要求。許多人不使用引號,而且很容易忘記,特別是如果文件的其餘部分(可能由其他人編寫)不使用引號。
它不方便
因為它太複雜了,它所聲稱的可移植性被誇大了。例如,考慮以下這個從 YAML 規範中獲取的示例:
? - Detroit Tigers
- Chicago cubs
:
- 2001-07-23
? [ New York Yankees,
Atlanta Braves ]
: [ 2001-07-02, 2001-08-12,
2001-08-14 ]
拋開大多數讀者可能甚至不知道這是在做什麼之外,請嘗試使用 PyYAML 在 Python 中解析它:
yaml.constructor.ConstructorError: while constructing a mapping
in "a.yaml", line 1, column 1
found unhashable key
in "a.yaml", line 1, column 3
在 Ruby 中,它可以工作:
{
["Detroit Tigers", "Chicago cubs"] => [
#<Date: 2001-07-23 ((2452114j,0s,0n),+0s,2299161j)>
],
["New York Yankees", "Atlanta Braves"] => [
#<Date: 2001-07-02 ((2452093j,0s,0n),+0s,2299161j)>,
#<Date: 2001-08-12 ((2452134j,0s,0n),+0s,2299161j)>,
#<Date: 2001-08-14 ((2452136j,0s,0n),+0s,2299161j)>
]
}
這個原因是你不能在 Python 中使用列表作為一個字典的鍵:
>>> {['a']: 'zxc'}
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'
而這種限制並不是 Python 特有的,PHP、JavaScript 和 Go 等常用語言都有此限制。
因此,在 YAML 文件中使用這種語法,你將無法在大多數語言中解析它。
這是另一個從 YAML 規範的示例部分中獲取的:
# Ranking of 1998 home runs
- Mark McGwire
- Sammy Sosa
- Ken Griffey
# Team ranking
- Chicago Cubs
- St Louis Cardinals
Python 會輸出:
yaml.composer.ComposerError: expected a single document in the stream
in "a.yaml", line 3, column 1
but found another document
in "a.yaml", line 8, column 1
然而 Ruby 輸出:
["Mark McGwire", "Sammy Sosa", "Ken Griffey"]
原因是單個文件中有多個 YAML 文檔(---
意味開始一個新文檔)。在 Python 中,有一個 load_all
函數來解析所有文檔,而 Ruby 的 load()
只是載入第一個文檔,據我所知,它沒有辦法載入多個文檔。
目標實現了嗎?
規範說明:
YAML 的設計目標安裝優先順序降序排列如下:
- YAML 很容易被人類閱讀。
- YAML 數據在編程語言之間是可移植的。
- YAML 匹配敏捷語言的原生數據結構。
- YAML 有一個一致的模型來支持通用工具。
- YAML 支持一次性處理。
- YAML 具有表現力和可擴展性。
- YAML 易於實現和使用。
那麼它做的如何呢?
YAML 很容易被人類閱讀。
只有堅持一小部分子集時才有效。完整的規則集很複雜 —— 遠遠超過 XML 或 JSON。
YAML 數據在編程語言之間是可移植的。
事實並非如此,因為創建常見語言不支持的結構太容易了。
YAML 匹配敏捷語言的原生數據結構。
參見上面。另外,為什麼只支持敏捷(或動態)語言?其他語言呢?
YAML 有一個一致的模型來支持通用工具。
我甚至不確定這意味著什麼,我找不到任何詳細說明。
YAML 支持一次性處理。
這點我接受。
YAML 具有表現力和可擴展性。
嗯,是的,但它太富有表現力(例如太複雜)。
YAML 易於實現和使用。
$ cat `ls -1 ~/gocode/src/github.com/go-yaml/yaml/*.go | grep -v _test` | wc -l
9247
$ cat /usr/lib/python3.5/site-packages/yaml/*.py | wc -l
5713
結論
不要誤解我的意思,並不是說 YAML 很糟糕 —— 它肯定不像使用 JSON 那麼多的問題 —— 但它也不是非常好。有一些一開始並不明顯的缺點和驚喜,還有許多更好的替代品,如 TOML 和其他更專業的格式。
就個人而言,當我有選擇時,我不太可能再次使用它。
如果你必須使用 YAML,那麼我建議你使用 StrictYAML,它會刪除一些(雖然不是全部)比較麻煩的部分。
反饋
你可以發送電子郵件至 martin@arp242.net 或創建 GitHub issue 以獲取反饋、問題等。
腳註
- 在 PHP 中你需要修改一個 INI 設置來獲得安全的行為,不能只是調用像
yaml_safe()
這樣的東西。PHP 想盡辦法讓愚蠢的東西越發愚蠢。幹得漂亮! ↩ - 不要在這裡做空格與製表符之爭,如果這裡可以用製表符的話,我可以(臨時)增加製表符寬度來使它更易讀——這是製表符的一種用途。 ↩
via: https://arp242.net/weblog/yaml_probably_not_so_great_after_all.html
作者:Martin Tournoij 選題:lujun9972 譯者:MjSeven 校對:wxy
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive